# EntityPersister.java

    package org.codefilarete.stalactite.engine;

    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.Consumer;
    import java.util.function.Function;

    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.SerializablePropertyAccessor;
    import org.codefilarete.reflection.SerializablePropertyMutator;
    import org.codefilarete.reflection.ValueAccessPoint;
    import org.codefilarete.stalactite.engine.EntityCriteria.CriteriaPath;
    import org.codefilarete.stalactite.engine.EntityCriteria.FluentOrderByClause;
    import org.codefilarete.stalactite.engine.EntityCriteria.SerializableCollectionFunction;
    import org.codefilarete.stalactite.engine.listener.PersisterListener;
    import org.codefilarete.stalactite.mapping.SimpleIdMapping;
    import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
    import org.codefilarete.stalactite.query.api.Selectable;
    import org.codefilarete.stalactite.query.model.ConditionalOperator;
    import org.codefilarete.stalactite.query.model.Select;
    import org.codefilarete.stalactite.sql.result.Accumulator;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Experimental;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.KeepOrderMap;
    import org.codefilarete.tool.collection.KeepOrderSet;
    import org.codefilarete.tool.collection.Maps;
    import org.codefilarete.tool.collection.PairIterator;

    /**
     * @author Guillaume Mary
     */
    public interface EntityPersister<C, I> extends PersistExecutor<C>, InsertExecutor<C>, UpdateExecutor<C>, SelectExecutor<C, I>, DeleteExecutor<C, I>, PersisterListener<C, I> {

    	/**
    	 * Persists an instance either it is already persisted or not (insert or update).
    	 *
    	 * Check between insert or update is determined by id state which itself depends on identifier policy,
    	 * see {@link SimpleIdMapping#IsNewDeterminer} implementations and
    	 * {@link IdentifierInsertionManager} implementations for id value computation.
    	 *
    	 * @param entity an entity to be persisted
    	 * @throws StaleStateObjectException if updated row count differs from entities count
    	 * @see #insert(Iterable)
    	 * @see #update(Iterable, boolean)
    	 */
    	default void persist(C entity) {
    		// determine insert or update operation
    		persist(Collections.singleton(entity));
    	}

    	/**
    	 * Choose either to insert or update entities according to their persistent state.
    	 *
    	 * @param entities entities to be inserted or updated according to {@link #isNew(Object)} result
    	 */
    	void persist(Iterable<? extends C> entities);

    	default void insert(C entity) {
    		insert(Collections.singletonList(entity));
    	}

    	/**
    	 * Updates an instance that may have changes.
    	 * Groups statements to benefit from JDBC batch. Useful overall when allColumnsStatement
    	 * is set to false.
    	 *
    	 * @param modified the supposing entity that has differences against {@code unmodified} entity
    	 * @param unmodified the "original" (freshly loaded from database ?) entity
    	 * @param allColumnsStatement true if all columns must be in the SQL statement, false if only modified ones should be in
    	 */
    	default void update(C modified, C unmodified, boolean allColumnsStatement) {
    		update(Collections.singletonList(new Duo<>(modified, unmodified)), allColumnsStatement);
    	}

    	/**
    	 * Updates given entity in database according to following mechanism : it selects the existing data in database, then compares it with given
    	 * entity in memory, and then updates database if necessary (nothing if no change was made).
    	 *
    	 * @param entity the entity to be updated
    	 */
    	default void update(C entity) {
    		update(entity, true);
    	}

    	/**
    	 * Updates given entity in database according to following mechanism : it selects the existing data in database, then compares it with given
    	 * entity in memory, and then updates database if necessary (nothing if no change was made).
    	 *
    	 * @param entity the entity to be updated
    	 * @param allColumnsStatement true to include all columns in statement even if only a part of them are touched, false to target only modified ones
    	 */
    	default void update(C entity, boolean allColumnsStatement) {
    		update(entity, select(getId(entity)), allColumnsStatement);
    	}

    	/**
    	 * Updates given entities in database according to following mechanism : it selects the existing data in database, then compares it with given
    	 * entities in memory, and then updates database if necessary (nothing if no change was made).
    	 * To be used for CRUD use case.
    	 *
    	 * @param entities the entities to be updated
    	 */
    	default void update(Iterable<C> entities) {
    		// Below we keep order of given entities mainly to get steady unit tests. Meanwhile, this may have performance
    		// impacts but very difficult to measure
    		Set<I> ids = Iterables.collect(entities, this::getId, KeepOrderSet::new);
    		Set<C> entitiesFromDb = select(ids);
    		// Given entities may not be in same order than loaded ones from DB, whereas order is required for comparison (else everything is different !)
    		// so we join them by their id to make them match
    		Map<C, I> idPerEntity = Iterables.map(entities, Function.identity(), this::getId, KeepOrderMap::new);
    		Map<I, C> entityFromDbPerId = Iterables.map(entitiesFromDb, this::getId, Function.identity());
    		Map<C, C> modifiedVsUnmodifiedEntities = Maps.innerJoinOnValuesAndKeys(idPerEntity, entityFromDbPerId, KeepOrderMap::new);
    		update(() -> new PairIterator<>(modifiedVsUnmodifiedEntities.keySet(), modifiedVsUnmodifiedEntities.values()), true);
    	}

    	/**
    	 * Helping method for "Command Design Pattern" so one can apply modifications to the entity loaded by its id without any concern of loading it.
    	 * This implementation will load twice the entity, invoke given {@link Consumer} with one of them, and then call {@link #update(Object, Object, boolean)} afterward.
    	 * Subclasses may override this behavior to enhance loading or change its algorithm (by using {@link #updateById(Iterable)} for instance)
    	 *
    	 * @param id key of entity to be modified
    	 * @param entityConsumer business code expected to modify its given entity
    	 */
    	@Experimental
    	default void update(I id, Consumer<C> entityConsumer) {
    		update(Collections.singleton(id), entityConsumer);
    	}

    	/**
    	 * Massive version of {@link #update(Object, Consumer)}. {@link Consumer} will be called for each found entities.
    	 *
    	 * @param ids keys of entities to be modified
    	 * @param entityConsumer business code expected to modify its given entity
    	 */
    	@Experimental
    	default void update(Iterable<I> ids, Consumer<C> entityConsumer) {
    		Set<C> unmodified = select(ids);
    		Set<C> modified = select(ids);
    		modified.forEach(entityConsumer);
    		update(() -> new PairIterator<>(modified, unmodified), true);
    	}

    	/**
    	 * Deletes instances.
    	 * Takes optimistic lock into account.
    	 *
    	 * @param entity entity to be deleted
    	 * @throws StaleStateObjectException if deleted row count differs from the entities count
    	 */
    	default void delete(C entity) {
    		delete(Collections.singletonList(entity));
    	}

    	/**
    	 * Will delete instances only by their identifier.
    	 * This method will not take optimistic lock (versioned entity) into account, so it will delete database rows "roughly".
    	 *
    	 * @param entity entity to be deleted
    	 */
    	default void deleteById(C entity) {
    		deleteById(Collections.singletonList(entity));
    	}

    	default C select(I id) {
    		return Iterables.first(select(Collections.singleton(id)));
    	}

    	default Set<C> select(I... ids) {
    		return select(Arrays.asHashSet(ids));
    	}

    	/**
    	 * Creates a query with some criteria based on some properties.
    	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
    	 * Raises an exception if the targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param getter a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableEntityQuery<C, ?> selectWhere(SerializablePropertyAccessor<C, O> getter, ConditionalOperator<O, ?> operator) {
    		return selectWhere(AccessorChain.fromMethodReference(getter), operator);
    	}

    	/**
    	 * Creates a query with some criteria based on some properties.
    	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param setter a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableEntityQuery<C, ?> selectWhere(SerializablePropertyMutator<C, O> setter, ConditionalOperator<O, ?> operator) {
    		return selectWhere(Arrays.asList(Accessors.mutatorByMethodReference(setter)), operator);
    	}

    	/**
    	 * Variation of {@link #selectWhere(SerializablePropertyAccessor, ConditionalOperator)} with a criteria on property of a property
    	 * Please note that whole bean graph is loaded, not only entities that satisfy criteria.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param getter1 a property accessor
    	 * @param getter2 a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O, A> ExecutableEntityQuery<C, ?> selectWhere(SerializablePropertyAccessor<C, A> getter1, SerializablePropertyAccessor<A, O> getter2, ConditionalOperator<O, ?> operator) {
    		return selectWhere(AccessorChain.fromMethodReferences(getter1, getter2), operator);
    	}

    	/**
    	 * Creates a query with some criteria based on some properties.
    	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param accessorChain a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableEntityQuery<C, ?> selectWhere(List<? extends ValueAccessPoint<?>> accessorChain, ConditionalOperator<O, ?> operator) {
    		return selectWhere().and(accessorChain, operator);
    	}

    	/**
    	 * Creates a query with some criteria based on some properties.
    	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param accessorChain a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableEntityQuery<C, ?> selectWhere(AccessorChain<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
    		return selectWhere(accessorChain.getAccessors(), operator);
    	}

    	default <O> ExecutableEntityQuery<C, ?> selectWhere(CriteriaPath<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
    		return selectWhere(accessorChain.getAccessors(), operator);
    	}

    	default <O, S extends Collection<O>, NEXT> ExecutableEntityQuery<C, ?> selectWhere(SerializableCollectionFunction<C, S, O> accessor1, SerializablePropertyAccessor<O, NEXT> accessor2, ConditionalOperator<NEXT, ?> operator) {
    		return selectWhere(new CriteriaPath<>(accessor1).add(accessor2), operator);
    	}

    	/**
    	 * Creates a query which criteria target mapped properties.
    	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
    	 *
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	ExecutableEntityQuery<C, ?> selectWhere();

    	/**
    	 * Creates a projection query which criteria target mapped properties.
    	 * {@link Select} must be modified by given select adapter (by default all column that would allow to load the entity are present).
    	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link Selectable}
    	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
    	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
    	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
    	 * <strong>Note that all {@link Selectable} added to the Select must have an alias</strong>.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param selectAdapter the {@link Select} clause modifier
    	 * @param getter a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializablePropertyAccessor<C, O> getter, ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter).and(getter, operator);
    	}

    	/**
    	 * Creates a projection query which criteria target mapped properties.
    	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
    	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link Selectable}
    	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
    	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
    	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
    	 * <strong>Note that all {@link Selectable} added to the Select must have an alias</strong>.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param selectAdapter the {@link Select} clause modifier
    	 * @param setter a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializablePropertyMutator<C, O> setter, ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter).and(setter, operator);
    	}

    	/**
    	 * Creates a projection query which criteria target mapped properties.
    	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
    	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link Selectable}
    	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
    	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
    	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
    	 * <strong>Note that all {@link Selectable} added to the Select must have an alias</strong>.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param selectAdapter the {@link Select} clause modifier
    	 * @param getter1 a property accessor
    	 * @param getter2 a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O, A> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializablePropertyAccessor<C, A> getter1, SerializablePropertyAccessor<A, O> getter2, ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter, AccessorChain.fromMethodReferences(getter1, getter2).getAccessors(), operator);
    	}

    	/**
    	 * Creates a projection query which criteria target mapped properties.
    	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
    	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link Selectable}
    	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
    	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
    	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
    	 * <strong>Note that all {@link Selectable} added to the Select must have an alias</strong>.
    	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
    	 *
    	 * @param selectAdapter the {@link Select} clause modifier
    	 * @param accessorChain a property accessor
    	 * @param operator criteria for the property
    	 * @param <O> value type returned by property accessor
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter,
    																	  List<? extends ValueAccessPoint<?>> accessorChain,
    																	  ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter).and(accessorChain, operator);
    	}

    	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter,
    																	  CriteriaPath<C, ?> accessorChain,
    																	  ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter, accessorChain.getAccessors(), operator);
    	}

    	default <O, S extends Collection<O>, NEXT> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter,
    																									 SerializableCollectionFunction<C, S, O> accessor1,
    																									 SerializablePropertyAccessor<O, NEXT> accessor2,
    																									 ConditionalOperator<O, ?> operator) {
    		return selectProjectionWhere(selectAdapter, new CriteriaPath<>(accessor1).add(accessor2), operator);
    	}

    	/**
    	 * Creates a projection query which criteria target mapped properties.
    	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
    	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link Selectable}
    	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
    	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
    	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
    	 * <strong>Note that all {@link Selectable} added to the Select must have an alias</strong>.
    	 *
    	 * @param selectAdapter the {@link Select} clause modifier
    	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
    	 */
    	ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter);

    	Set<C> selectAll();

    	boolean isNew(C entity);

    	I getId(C entity);

    	Class<C> getClassToPersist();

    	/**
    	 * Mashup between {@link EntityCriteria} and {@link ExecutableQuery} to make an {@link EntityCriteria} executable
    	 * @param <C> type of object returned by query execution
    	 */
    	interface ExecutableEntityQuery<C, SELF extends ExecutableEntityQuery<C, SELF>> extends EntityCriteria<C, SELF>, ExecutableQuery<C>, FluentOrderByClause<C, SELF> {

    		SELF set(String paramName, Object paramValue);

    		/**
    		 * Overridden for a more accurate return type.
    		 * {@inheritDoc}
    		 */
    		ExecutableEntityQuery<C, SELF> beginNested();

    		/**
    		 * Overridden for a more accurate return type.
    		 * {@inheritDoc}
    		 */
    		ExecutableEntityQuery<C, SELF> endNested();

    	}

    	/**
    	 * Mashup between {@link EntityCriteria} and {@link ExecutableProjection} to make an {@link EntityCriteria} executable
    	 * @param <C> type of object returned by query execution
    	 */
    	interface ExecutableProjectionQuery<C, SELF extends ExecutableProjectionQuery<C, SELF>> extends EntityCriteria<C, SELF>, ExecutableProjection, FluentOrderByClause<C, SELF> {

    		SELF set(String paramName, Object paramValue);

    		/**
    		 * Overridden for a more accurate return type.
    		 * {@inheritDoc}
    		 */
    		ExecutableProjectionQuery<C, SELF> beginNested();

    		/**
    		 * Overridden for a more accurate return type.
    		 * {@inheritDoc}
    		 */
    		ExecutableProjectionQuery<C, SELF> endNested();
    	}

    	/**
    	 * Abstraction to configure the select clause of a query.
    	 *
    	 * @param <C> the type of context or entity for which the selection is configured
    	 */
    	interface SelectAdapter<C> {

    		Set<Selectable<?>> getColumns();

    		SelectAdapter<C> distinct();

    		SelectAdapter<C> setDistinct(boolean distinct);

    		SelectAdapter<C> add(Selectable<?> column);

    		SelectAdapter<C> add(Selectable<?> column, String alias);

    		default SelectAdapter<C> add(CriteriaPath<C, ?> property) {
    			return add(property.getAccessors());
    		}

    		default SelectAdapter<C> add(CriteriaPath<C, ?> property, String alias) {
    			return add(property.getAccessors(), alias);
    		}

    		default SelectAdapter<C> add(List<ReadWritePropertyAccessPoint<?, ?>> property) {
    			return add(giveColumn(property));
    		}

    		default SelectAdapter<C> add(List<ReadWritePropertyAccessPoint<?, ?>> property, String alias) {
    			return this.add(giveColumn(property), alias);
    		}

    		default Selectable<?> giveColumn(ReadWritePropertyAccessPoint<?, ?> property) {
    			return giveColumn(Arrays.asList(property));
    		}

    		default Selectable<?> giveColumn(SerializablePropertyAccessor<C, ?> property) {
    			return giveColumn(Accessors.readWriteAccessPoint(property));
    		}

    		default Selectable<?> giveColumn(SerializablePropertyMutator<C, ?> property) {
    			return giveColumn(Accessors.readWriteAccessPoint(property));
    		}

    		Selectable<?> giveColumn(List<ReadWritePropertyAccessPoint<?, ?>> property);
    	}
    }